/**
 * AGNObject.java
 */

package agngui.agnmodel;
import javax.media.j3d.Appearance;
import javax.media.j3d.Geometry;
import javax.media.j3d.Material;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.RenderingAttributes;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransparencyAttributes;
import javax.vecmath.AxisAngle4d;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Quat4d;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import agngui.event.AGNEvent;
import agngui.event.AGNListener;
import agngui.event.AGNPublisher;
import agngui.event.ObjectEvent;
import agngui.event.ObjectHideEvent;
import agngui.event.ObjectReleaseEvent;
import agngui.event.ObjectSelectEvent;
import agngui.event.ObjectShowEvent;
import agngui.ui.ConfigWindow;

/**
 * Abstract class generically describing AGN component geometry.
 */
public abstract class AGNObject extends Shape3D implements AGNListener {

	/**
	 * Inner Radius identifier
	 */
    public static final String ID_RI = ConfigWindow.common_strings[0].trim();
    
    /**
     * Outer Radius identifier
     */
    public static final String ID_RO = ConfigWindow.common_strings[1].trim();
    
    /**
     * Polar angle identifier
     */
    public static final String ID_ROT_POLAR       = ConfigWindow.common_strings[2].trim();
    
    /**
     * Azimuthal angle identifier
     */
    public static final String ID_ROT_AZIMUTHAL   = ConfigWindow.common_strings[3].trim();
    
    /**
     * Half angle identifier
     */
    public static final String ID_HA              = ConfigWindow.common_strings[4].trim();
    
    /**
     * Outer half angle identifier
     */
    public static final String ID_HAO             = ConfigWindow.wind_strings[0].trim();
    
    /**
     * One light-year in meters, used for scaling
     */
	public static final double ONE_DISTANCE_UNIT = 9.4605284e15;//1 light-year in meters.
	
	/**
	 * inverse of one light year in meters
	 */
	public static final double SCALE_FACTOR = 1/(ONE_DISTANCE_UNIT);
	
	/**
	 * The default transparency for objects in the system
	 */
	public static final float DEFAULT_TRANSPARENCY = 0.5f;
	
	/**
	 * The transparency for a non-selected object when other objects are selected
	 */
	private static final float NON_SELECTED_OBJECT_TRANSPARENCY = 0.80f;
	
	/**
	 * The transparency for a selected object
	 */
	private static final float SELECTED_OBJECT_TRANSPARENCY = 0.20f;
	
	/**
	 * The standard polar axis of rotation
	 */
	protected static final Vector3d POLAR_AXIS_OF_ROTATION     = new Vector3d(1, 0, 0);
	
	/**
	 * The standard azimuthal axis of rotation
	 */
	protected static final Vector3d AZIMUTHAL_AXIS_OF_ROTATION = new Vector3d(0, 0, 1);
	
	/**
	 * The array of all vertex points for this object
	 */
	protected Point3d[] point_array;
	
	/**
	 * The array of normals at every vertex point, should match the point_array vertices
	 */
	protected Vector3f[] norm_array;
	
	/**
	 * The geometry that allows for drawing objects
	 */
    protected AGNModelGeometry modelGeometry;
    
    /**
     * The transform group to move the whole system around if necessary 
     */
    protected TransformGroup   systemTransformGroup;
    
    /**
     * a flag that denotes whether or not the object is showing
     */
    protected boolean is_showing;

    /**
     * The inner radius of this object
     */
    protected double Ri;    // Inner radius
    
    /**
     * The outer radius of this object
     */
    protected double Ro;    // Outer radius
    
    /**
     * The half opening angle of this object
     */
    protected double ha;    // Half angle
    
    /**
     * The outer half opening angle (if applicable)
     */
    protected double hao;   // Half angle outer (wind)
    
    /**
     * The number of facets for this object (if applicable)
     */
    protected int    facets;// Number of facets (torus)
    
    /**
     * The number of points to use
     */
    protected int    n;     // Number of facets

    /**
     * the object transform
     */
    protected TransformGroup transformGroup;   /* The object transform                     */
    
    /**
     * The parameter handles for this object
     */
    protected AGNParameterHandle handles[];    /* The parameter handles for this object    */
    
    /**
     * The geometry mesh that represents this object
     */
    protected Geometry       geometry[];       /* Mesh geometry                            */
    
    /**
     * an identifier for this object
     */
    private   String         identifier;       /* Signifies the object type                */
    
    /**
     * the polar rotation angle this object is currently using
     */
    protected double polar_rotation;
    
    /**
     * the azimuthal rotation angle that this object is currently using
     */
    protected double azimuthal_rotation;
    
    /**
     * diffuse color for this object, used in turning on/off shading
     */
    private Color3f saved_diffuse_color;
    
    
	
    /**
     * AGNObject
     *
     * Initializes the object geometry; central axis = system axis. 
     *
     * @param identifier    associates the object with a particular geometry.
     * @param modelGeometry the geometry object this object is being stored into
     */
    public AGNObject( String identifier, AGNModelGeometry modelGeometry ) {
        transformGroup     = new TransformGroup();
        this.identifier    = identifier;
        this.modelGeometry = modelGeometry;
        is_showing = false;
        this.polar_rotation = 0;
        this.azimuthal_rotation = 0;

        systemTransformGroup = modelGeometry.getSystemTransformGroup();

		Transform3D xform = new Transform3D();
		transformGroup.getTransform(xform);
		xform.setScale(SCALE_FACTOR);
		transformGroup.setTransform(xform);
        transformGroup.setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE );
        transformGroup.setCapability( TransformGroup.ALLOW_TRANSFORM_READ  );
		
        handles = null;
		
		setCapability(ALLOW_GEOMETRY_WRITE);
		setCapability(ALLOW_APPEARANCE_OVERRIDE_READ);
		setCapability(ALLOW_APPEARANCE_OVERRIDE_WRITE);
		setCapability(ALLOW_APPEARANCE_READ);
		setCapability(ALLOW_APPEARANCE_WRITE);
		setCapability(ALLOW_PICKABLE_READ);
		setCapability(ALLOW_PICKABLE_WRITE);
		
		Appearance app = new Appearance();
		
		app.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_WRITE);
		app.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_READ);
		app.setCapability(Appearance.ALLOW_POLYGON_ATTRIBUTES_READ);
		app.setCapability(Appearance.ALLOW_POLYGON_ATTRIBUTES_WRITE);
		app.setCapability(PolygonAttributes.ALLOW_MODE_READ);
		app.setCapability(PolygonAttributes.ALLOW_MODE_WRITE);
		app.setCapability(Appearance.ALLOW_MATERIAL_READ);
		app.setCapability(Appearance.ALLOW_MATERIAL_WRITE);
		
		RenderingAttributes ra = new RenderingAttributes();
		ra.setDepthBufferEnable(true);
		ra.setDepthTestFunction(RenderingAttributes.LESS);
		
		Material material = new Material();
		material.setCapability(Material.ALLOW_COMPONENT_READ);
		material.setCapability(Material.ALLOW_COMPONENT_WRITE);
		material.setSpecularColor(new Color3f(0,0,0));
		material.setShininess(1);//disable..  1.0 = not shiny, values in range [1.0, 128.0] are accepted
		
		app.setRenderingAttributes(ra);
		app.setMaterial(material);
		setAppearance(app);

        setPickable( true );
        
        AGNPublisher.addAGNListener(this);
    }
    
    /**
     * gives the vertices that make up this object
     * 
     * @return the array of vertices that represent this object
     */
	public Point3d[] getPointArray() { return point_array; }
	
	/**
	 * gives the normal array of this object
	 * 
	 * @return this object's array of normal vectors
	 */
	public Vector3f[] getNormArray() { return norm_array; }
    
    /**
     * setShading
     *
     * Sets this object's shading mode.
     *
     * @param mode The Java3D polygon mode to be used for this object's appearance.
     */
    public void setShadingMode(int mode) {
    	Appearance app = getAppearance();
    	
    	PolygonAttributes polyAttrib = new PolygonAttributes();
    	polyAttrib.setPolygonMode(mode);
    	app.setPolygonAttributes(polyAttrib);
    	
    	setAppearance(app);
    }

    /**
     * restoreMaterial
     *
     * Restores the previously saved material for this object
     *
     * @param app this object's appearance
     */
    private void restoreMaterial(Appearance app) {
    	if(saved_diffuse_color != null) {
	    	Material mat = app.getMaterial();
	    	Color3f ambient_color = new Color3f();
	    	mat.getAmbientColor(ambient_color);
	    	Vector3f ambient_vector = new Vector3f(ambient_color);
	    	ambient_vector.scale(0.25f);
	    	
	    	mat.setDiffuseColor(saved_diffuse_color);
	    	mat.setAmbientColor(new Color3f(ambient_vector));
	    	app.setMaterial(mat);
	    	saved_diffuse_color = null;
    	}
    }
    
    /**
     * enableShading
     *
     * Enables shading on this object
     */
    public void enableShading(boolean shade) {
    	if(shade) {
    		Appearance app = getAppearance();
    		restoreMaterial(app);

    		setAppearance(app);
    	} else {
    		if(saved_diffuse_color == null) {
        		Appearance app = getAppearance();
            	Material mat = app.getMaterial();
            	
        		saved_diffuse_color = new Color3f();
        		mat.getDiffuseColor(saved_diffuse_color);
        		Color3f ambient_color = new Color3f();
        		mat.getAmbientColor(ambient_color);
        		
        		mat.setDiffuseColor(new Color3f());
        		Vector3f maxed_ambient = new Vector3f(ambient_color);
        		maxed_ambient.normalize();
        		mat.setAmbientColor(new Color3f(maxed_ambient));
        		app.setMaterial(mat);

            	setAppearance(app);
        	}
    	}
    }
    
    /**
     * setShowing
     *
     * Makes the object visible or invisible.
     * 
     * @param showing if true, the object is set to visible; if false, the object is made invisible
     */
    public void setShowing(boolean showing) { 
    	is_showing = showing;
    	if(!is_showing) {
    		setTransparency(1.0f);
    	} else {
    		setTransparency(DEFAULT_TRANSPARENCY);
    	}
    	setPickable(is_showing);
    }

    /**
     * isShowing
     *
     * @return true if the object is visible, false otherwise
     */
    public boolean isShowing() { return is_showing; }

    /**
     * processes events which the Object needs to know about
     */
    public void processEvent(AGNEvent event) {
    	if(event instanceof ObjectEvent) {
    		AGNObject object = ((ObjectEvent)event).getObject();
    		if(event instanceof ObjectSelectEvent) {
    			if(object == this) {
    				setTransparency(SELECTED_OBJECT_TRANSPARENCY);
    			} else {
    				if(is_showing) {
    					setTransparency(NON_SELECTED_OBJECT_TRANSPARENCY);
    				}
    			}
    		} else if(event instanceof ObjectReleaseEvent) {
    			if(is_showing) {
    				setTransparency(DEFAULT_TRANSPARENCY);
    			}
    		} else if(event instanceof ObjectShowEvent) {
    			if(object == this) {
    				setShowing(true);
    			}
    		} else if(event instanceof ObjectHideEvent) {
    			if(object == this) {
    				setShowing(false);
    			}
    		}
    	}
    }
    
    /**
     * getVertexNormal
     * 
     * Given a point, and two vertices that are on the cone with it, create
     * the normal. 
     * 
     * @param point The point of interest on the object
     * @param vertical The point vertically adjacent to point
     * @param horizontal The point horizontally adjacent to point
     * @return The vector normal to the triangle defined by point, vertical and horizontal
     */
    protected Vector3f getVertexNormal(Point3d point, Point3d vertical, Point3d horizontal) {
		Vector3f normal = new Vector3f();

        Vector3f v1 = new Vector3f();
        v1.sub(new Vector3f(point), new Vector3f(horizontal));
        v1.normalize();
        
        Vector3f v2 = new Vector3f();
        v2.sub(new Vector3f(point), new Vector3f(vertical));
        v2.normalize();
        normal.sub(v1, v2);
        normal.scale(0.5f);
        normal.add(v2);
        
        normal.normalize();
        
        return normal;
    }

    /**
     * addChild
     * 
     * Makes this object the parent of another object.
     *
     * @param object The child object
     */
    public void addChild( AGNObject object ) {
        transformGroup.addChild( object.getTransformGroup() );
    }

    /**
     * getDimensionHandleLocations
     * 
     * @return an array of Point3ds that specify the locations in world space
     */
    public Point3d[] getDimensionHandleLocations() {
        Point3d points[] = new Point3d[handles.length];

        for( int i = 0; i < points.length; ++i ) {
            AGNParameterHandle handle = handles[i];
            points[i] = new Point3d( handle.getPoint() );
        }

        return points;
    }

    /**
     * getIntersectingHandle
     *
     * Checks to see if the mouse coordinates click on any of the handles.
     *
     * @param mX The mouse x-coordinate
     * @param mY The mouse y-coorinate
     * @param canvas Reference to the canvas drawing the HUD
     * @return a reference to the intersecting handle, or none if none intersect.
     */
    public AGNParameterHandle getIntersectingHandle( int mX, int mY, AGNModelCanvas canvas ) {
        AGNParameterHandle retVal = null;

        for( AGNParameterHandle handle : handles ) {
            if( handle.isClickedOn( mX, mY, canvas ) ) {
                retVal = handle;
                break;
            }
        }
    
        return retVal;    
    }

    /**
     * generateHandles
     *
     * Produces the list of handles and throws 'em in the appropriate place.
     */
    public abstract void generateHandles();

    /**
     * gives the diffuse color of this object
     * 
     * @return the diffuse color of this object
     */
    public Color3f getDiffuseColor() {
    	return saved_diffuse_color;
    }

    /**
     * getFullTransform
     *
     * @return the system + object transform
     */
    public Transform3D getFullTransform() {
        Transform3D temp          = new Transform3D();
        Transform3D fullTransform = new Transform3D();

        systemTransformGroup.getTransform( fullTransform );
        transformGroup.getTransform( temp );
        fullTransform.mul( temp );

        return fullTransform;
    }

    /**
     * getHandles
     * 
     * @return the array of parameter handles associated with this object
     */
    public AGNParameterHandle[] getHandles() {
        return handles;
    }
	
    /**
     * returns the handle with the given ID, or null if no such handle exists
     * 
     * @param id The identifier for the handle of interest
     * @return a reference to the object handle identified by id
     */
	public AGNParameterHandle getHandle(String id) {
		AGNParameterHandle ret_handle = null;
		
		for(AGNParameterHandle h : handles) {
			if(h.getIdentifier().equals(id)) {
				ret_handle = h;
				break;
			}
		}
		
		return ret_handle;
	}

    /**
     * getTransformGroup
     * 
     * @return the local transformGroup object
     */
    public TransformGroup getTransformGroup() {
        return transformGroup;
    }

    /**
     * getGeometry
     *
     * @return the Geometry at index 0
     */
    public Geometry getGeometry() {
        return geometry[0];
    }
	
    /**
     * getIdentifier
     *
     * @return The identifier for this object.
     */
    public String getIdentifier() {
        return identifier;
    }

    /**
     * sets this object's transparency to the given value
     * 
     * @param transparency the transparency value to set
     */
	public void setTransparency(float transparency) {
		Appearance appearance = getAppearance();
        
		int style = TransparencyAttributes.NICEST;
		if(transparency == 0) {
			style = TransparencyAttributes.NONE;
		}
		TransparencyAttributes transAttrib = new TransparencyAttributes(style, transparency);
		
		appearance.setTransparencyAttributes(transAttrib);
		setAppearance(appearance);
	}
	
	/**
	 * sets this object's color
	 * 
	 * @param color The color to set this object to.
	 */
	public void setColor(Color3f color) {
		Appearance appearance = getAppearance();
		Vector3f vec = new Vector3f(color);
		Vector3f amb = new Vector3f(color);
		vec.scale(0.75f);
		amb.scale(0.25f);
		
		Color3f diffuse_color = new Color3f(vec);
		Color3f ambient_color = new Color3f(amb);
		
		if(saved_diffuse_color != null) {//shading is turned off
			saved_diffuse_color = diffuse_color;
		}
		Material mat = appearance.getMaterial();
		mat.setAmbientColor(ambient_color);
		mat.setDiffuseColor(diffuse_color);
		
		setAppearance(appearance);
	}
	
    /**
     * createGeometry
     *
     * Regenerates the geometry of an object based on its internal state.
     */
    protected abstract Geometry[] createGeometry();

    /**
     * resize
     *
     * Regenerates the object's geometry based on a changed handle parameter, either
     * through a UI field or through the release of a HUD element.
     *
     * @param handle A reference to the handle released, if this resize is the result of a
     *               released HUD element.
     * @param new_value The new value for the handle
     */
    public void resize(AGNParameterHandle handle, Double new_value) {
		Transform3D transform = new Transform3D();
		
		// Determine which parameter corresponds to the handle
		String id = handle.getIdentifier();
		if( id.equals( ID_RO ) ) {
			double new_ro = new_value;

			if( new_ro > Ri ) {
                Ro = new_ro;
			}
		} else if( id.equals( ID_RI ) ) {
			double new_ri = new_value; 
            
			if( new_ri < Ro ) {
				Ri = new_ri;
			}
            
		} else if( id.equals( ID_HA ) ) {
            double newHa = new_value;

            if( this instanceof AGNWind && newHa > (hao * Math.PI / 180 ) ) {
                return;
            }
			
            if( newHa < (Math.PI / 2) && newHa > 0 ) {
			    ha = newHa * 180 / Math.PI;
            }
		} else if( id.equals( ID_HAO ) ) {
            double newHao = new_value;

            if( newHao < (Math.PI / 2) && newHao > 0 && newHao > (ha * Math.PI / 180) ) {
                hao = newHao * 180 / Math.PI;
            }
        } else if( id.equals( ID_ROT_AZIMUTHAL )   || 
                   id.equals( ID_ROT_POLAR ) ) {
            AGNRotationalParameterHandle rotHandle = (AGNRotationalParameterHandle)handle;
            Quat4d      rotQuat      = new Quat4d();
            double      theta        = new_value;
            Vector3d azim_axis       = new Vector3d(AZIMUTHAL_AXIS_OF_ROTATION);
            Vector3d polar_axis      = new Vector3d(POLAR_AXIS_OF_ROTATION);

            transformGroup.getTransform( transform );
            
            if(id.equals(ID_ROT_AZIMUTHAL)) {
            	azimuthal_rotation = theta;
            	Transform3D inverseTransform = new Transform3D();
            	inverseTransform.invert( transform );
            	inverseTransform.transform( azim_axis );
            } else {//has to be polar
                // Transform the axis to be in the x-y plane, azimuthalangle degrees from the y-axis
                Transform3D axisTransform = new Transform3D();
                AxisAngle4d axisAngle     = new AxisAngle4d(azim_axis, azimuthal_rotation);

                axisTransform.setRotation( axisAngle );
                axisTransform.transform( polar_axis );

            	if(theta > Math.PI) {//polar angle cannot exceed 180 degrees.
                	return;
                }
            	polar_rotation = theta;
            }
            
            Quat4d polar_quat = new Quat4d();
            polar_quat.set(new AxisAngle4d(polar_axis, polar_rotation));
            
            Quat4d azimuthal_quat = new Quat4d();
            azimuthal_quat.set(new AxisAngle4d(azim_axis, azimuthal_rotation));
            
            rotQuat.mul(polar_quat, azimuthal_quat);
            
            transform.setRotation(rotQuat);
            
            transformGroup.setTransform( transform );
            rotHandle.setRecentValue( new_value );
        } else {
            throw new IllegalArgumentException( "Invalid handle type in AGNObject.resize: " + id );
        }
		
		// Generate new geometry
		removeAllGeometries();
        
        geometry = createGeometry();
        for(Geometry g : geometry) {
			addGeometry(g);
		}
    }
    
    /**
     * shell for calling resize(Handle, double)
     * @param handle The handle to use in resizing
     */
	public void resize(AGNParameterHandle handle) {
		resize(handle, handle.getRecentValue());
	}    

}
